package mirrormonkey.framework;

import java.util.Collection;

import mirrormonkey.core.EntityLifecycleListener;
import mirrormonkey.core.messages.TimeSyncRequestMessage;
import mirrormonkey.core.messages.TimeSyncResponseMessage;
import mirrormonkey.util.listeners.ListenerConfiguration;

import com.jme3.network.Message;
import com.jme3.network.MessageConnection;
import com.jme3.system.NanoTimer;
import com.jme3.system.Timer;

public class ConnectionInfo {

	protected static final int BURST_SIZE = 5;

	protected static final float MIN_BURST_FREQ = 2f;

	protected static final float MAX_BURST_FREQ = 0.5f;

	private final ListenerConfiguration listenerConfiguration = new ListenerConfiguration();

	protected final SyncAppState appState;

	protected final MessageConnection connection;

	protected final long[] sentRequests;

	protected final long[] travelTimes;

	protected final long[] receivedTimes;

	protected int receivedCount = 0;

	protected final TimeSyncResponseMessage[] receivedResponses;

	protected int requestBound;

	protected int requestId;

	protected float nextBurst;

	protected long estimatedRemoteTime;

	protected long estimatedLatency;

	protected final Timer t = new NanoTimer();

	protected boolean registered;

	protected boolean running;

	public ConnectionInfo(SyncAppState appState, MessageConnection connection,
			boolean registered) {
		if (connection == null) {
			throw new NullPointerException();
		}
		this.appState = appState;
		this.connection = connection;
		sentRequests = new long[BURST_SIZE];
		travelTimes = new long[BURST_SIZE];
		receivedTimes = new long[BURST_SIZE];
		receivedResponses = new TimeSyncResponseMessage[BURST_SIZE];
		requestBound = 0;
		requestId = 0;
		nextBurst = MAX_BURST_FREQ;
		estimatedRemoteTime = 0;
		estimatedLatency = 0;
		this.registered = registered;
		running = false;
	}

	public boolean isRegistered() {
		return registered;
	}

	public void update(float tpf) {
		t.update();
		estimatedRemoteTime += t.getTime();
		t.reset();
		if ((nextBurst -= tpf) <= 0) {
			evaluateBurst();
			try {
				sendBurst();
			} catch (Exception e) {
				e.printStackTrace();
			}
			nextBurst = MIN_BURST_FREQ;
		}
	}

	protected void evaluateBurst() {
		long accDelta = 0;
		long accTravelTime = 0;
		for (int i = 0; i < BURST_SIZE; i++) {
			TimeSyncResponseMessage m = receivedResponses[i];
			if (m != null) {
				accTravelTime += travelTimes[i];
				accDelta += (m.localTime - receivedTimes[i]);
			}
		}
		if (receivedCount != 0) {
			estimatedLatency = accTravelTime / (2 * receivedCount);
			estimatedRemoteTime += (accDelta / receivedCount);
			estimatedRemoteTime += estimatedLatency;
		}
		receivedCount = 0;
	}

	protected void sendBurst() {
		for (int i = 0; i < BURST_SIZE; i++) {
			receivedResponses[i] = null;
			sentRequests[i] = estimatedRemoteTime;
			send(new TimeSyncRequestMessage(requestId++));
		}
		requestBound += BURST_SIZE;
	}

	public void processTimeSyncResponse(TimeSyncResponseMessage r) {
		if (r.id < requestBound) {
			int bufferId = r.id % BURST_SIZE;
			travelTimes[bufferId] = estimatedRemoteTime
					- sentRequests[bufferId];
			receivedTimes[bufferId] = estimatedRemoteTime;
			receivedResponses[bufferId] = r;
			if (++receivedCount == BURST_SIZE) {
				nextBurst = MAX_BURST_FREQ;
			}
		}
	}

	@Override
	public String toString() {
		return "estimated latency: " + estimatedLatency
				+ "\nestimated remote time: " + estimatedRemoteTime;
	}

	protected ListenerConfiguration getListenerConfiguration() {
		return listenerConfiguration;
	}

	public void addEntityLifecycleListener(EntityLifecycleListener listener) {
		listenerConfiguration.addListener(listener);
	}

	public void collectListeners(Collection<EntityLifecycleListener> l) {
		listenerConfiguration.getListeners(EntityLifecycleListener.class, l);
	}

	public void removeEntityLifecycleListener(EntityLifecycleListener listener) {
		listenerConfiguration.removeListener(listener);
	}

	public long getEstimatedRemoteTime() {
		return estimatedRemoteTime;
	}

	public long getEstimatedLatency() {
		return estimatedLatency;
	}

	public void send(Message message) {
		if (!running) {
			return;
		}
		connection.send(message);
	}

	public MessageConnection getConnection() {
		return connection;
	}

	public boolean isRunning() {
		return running;
	}
}
